/*
 * Copyright (C) 2013 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.squareup.picasso;

import android.Manifest;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;

import static android.content.Context.CONNECTIVITY_SERVICE;
import static android.content.Intent.ACTION_AIRPLANE_MODE_CHANGED;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static com.squareup.picasso.BitmapHunter.forRequest;

class Dispatcher {
	private static final int RETRY_DELAY = 500;
	private static final int AIRPLANE_MODE_ON = 1;
	private static final int AIRPLANE_MODE_OFF = 0;

	static final int REQUEST_SUBMIT = 1;
	static final int REQUEST_CANCEL = 2;
	static final int REQUEST_GCED = 3;
	static final int HUNTER_COMPLETE = 4;
	static final int HUNTER_RETRY = 5;
	static final int HUNTER_DECODE_FAILED = 6;
	static final int HUNTER_DELAY_NEXT_BATCH = 7;
	static final int HUNTER_BATCH_COMPLETE = 8;
	static final int NETWORK_STATE_CHANGE = 9;
	static final int AIRPLANE_MODE_CHANGE = 10;

	private static final String DISPATCHER_THREAD_NAME = "Dispatcher";
	private static final int BATCH_DELAY = 200; // ms

	final DispatcherThread dispatcherThread;
	final Context context;
	final ExecutorService service;
	final Downloader downloader;
	final Map<String, BitmapHunter> hunterMap;
	final Handler handler;
	final Handler mainThreadHandler;
	final Cache cache;
	final Stats stats;
	final List<BitmapHunter> batch;
	final NetworkBroadcastReceiver receiver;

	NetworkInfo networkInfo;
	boolean airplaneMode;

	Dispatcher(Context context, ExecutorService service,
			Handler mainThreadHandler, Downloader downloader, Cache cache,
			Stats stats) {
		this.dispatcherThread = new DispatcherThread();
		this.dispatcherThread.start();
		this.context = context;
		this.service = service;
		this.hunterMap = new LinkedHashMap<String, BitmapHunter>();
		this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
		this.downloader = downloader;
		this.mainThreadHandler = mainThreadHandler;
		this.cache = cache;
		this.stats = stats;
		this.batch = new ArrayList<BitmapHunter>(4);
		this.airplaneMode = Utils.isAirplaneModeOn(this.context);
		this.receiver = new NetworkBroadcastReceiver(this.context);
		receiver.register();
	}

	@TargetApi(Build.VERSION_CODES.ECLAIR)
	void shutdown() {
		service.shutdown();
		dispatcherThread.quit();
		receiver.unregister();
	}

	@SuppressWarnings("rawtypes")
	void dispatchSubmit(Action action) {
		handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
	}

	@SuppressWarnings("rawtypes")
	void dispatchCancel(Action action) {
		handler.sendMessage(handler.obtainMessage(REQUEST_CANCEL, action));
	}

	void dispatchComplete(BitmapHunter hunter) {
		handler.sendMessage(handler.obtainMessage(HUNTER_COMPLETE, hunter));
	}

	void dispatchRetry(BitmapHunter hunter) {
		handler.sendMessageDelayed(handler.obtainMessage(HUNTER_RETRY, hunter),
				RETRY_DELAY);
	}

	void dispatchFailed(BitmapHunter hunter) {
		handler.sendMessage(handler.obtainMessage(HUNTER_DECODE_FAILED, hunter));
	}

	void dispatchNetworkStateChange(NetworkInfo info) {
		handler.sendMessage(handler.obtainMessage(NETWORK_STATE_CHANGE, info));
	}

	void dispatchAirplaneModeChange(boolean airplaneMode) {
		handler.sendMessage(handler.obtainMessage(AIRPLANE_MODE_CHANGE,
				airplaneMode ? AIRPLANE_MODE_ON : AIRPLANE_MODE_OFF, 0));
	}

	@SuppressWarnings("rawtypes")
	void performSubmit(Action action) {
		BitmapHunter hunter = hunterMap.get(action.getKey());
		if (hunter != null) {
			hunter.attach(action);
			return;
		}

		if (service.isShutdown()) {
			return;
		}

		hunter = forRequest(context, action.getPicasso(), this, cache, stats,
				action, downloader);
		hunter.future = service.submit(hunter);
		hunterMap.put(action.getKey(), hunter);
	}

	@SuppressWarnings("rawtypes")
	void performCancel(Action action) {
		String key = action.getKey();
		BitmapHunter hunter = hunterMap.get(key);
		if (hunter != null) {
			hunter.detach(action);
			if (hunter.cancel()) {
				hunterMap.remove(key);
			}
		}
	}

	void performRetry(BitmapHunter hunter) {
		if (hunter.isCancelled())
			return;

		if (service.isShutdown()) {
			performError(hunter);
			return;
		}

		if (hunter.shouldRetry(airplaneMode, networkInfo)) {
			hunter.future = service.submit(hunter);
		} else {
			performError(hunter);
		}
	}

	void performComplete(BitmapHunter hunter) {
		if (!hunter.shouldSkipMemoryCache()) {
			cache.set(hunter.getKey(), hunter.getResult());
		}
		hunterMap.remove(hunter.getKey());
		batch(hunter);
	}

	void performBatchComplete() {
		List<BitmapHunter> copy = new ArrayList<BitmapHunter>(batch);
		batch.clear();
		mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(
				HUNTER_BATCH_COMPLETE, copy));
	}

	void performError(BitmapHunter hunter) {
		hunterMap.remove(hunter.getKey());
		batch(hunter);
	}

	void performAirplaneModeChange(boolean airplaneMode) {
		this.airplaneMode = airplaneMode;
	}

	void performNetworkStateChange(NetworkInfo info) {
		networkInfo = info;
		if (service instanceof PicassoExecutorService) {
			((PicassoExecutorService) service).adjustThreadCount(info);
		}
	}

	private void batch(BitmapHunter hunter) {
		if (hunter.isCancelled()) {
			return;
		}
		batch.add(hunter);
		if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {
			handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH,
					BATCH_DELAY);
		}
	}

	private static class DispatcherHandler extends Handler {
		private final Dispatcher dispatcher;

		public DispatcherHandler(Looper looper, Dispatcher dispatcher) {
			super(looper);
			this.dispatcher = dispatcher;
		}

		@SuppressWarnings("rawtypes")
		@Override
		public void handleMessage(final Message msg) {
			switch (msg.what) {
			case REQUEST_SUBMIT: {
				Action action = (Action) msg.obj;
				dispatcher.performSubmit(action);
				break;
			}
			case REQUEST_CANCEL: {
				Action action = (Action) msg.obj;
				dispatcher.performCancel(action);
				break;
			}
			case HUNTER_COMPLETE: {
				BitmapHunter hunter = (BitmapHunter) msg.obj;
				dispatcher.performComplete(hunter);
				break;
			}
			case HUNTER_RETRY: {
				BitmapHunter hunter = (BitmapHunter) msg.obj;
				dispatcher.performRetry(hunter);
				break;
			}
			case HUNTER_DECODE_FAILED: {
				BitmapHunter hunter = (BitmapHunter) msg.obj;
				dispatcher.performError(hunter);
				break;
			}
			case HUNTER_DELAY_NEXT_BATCH: {
				dispatcher.performBatchComplete();
				break;
			}
			case NETWORK_STATE_CHANGE: {
				NetworkInfo info = (NetworkInfo) msg.obj;
				dispatcher.performNetworkStateChange(info);
				break;
			}
			case AIRPLANE_MODE_CHANGE: {
				dispatcher
						.performAirplaneModeChange(msg.arg1 == AIRPLANE_MODE_ON);
				break;
			}
			default:
				Picasso.HANDLER.post(new Runnable() {
					@Override
					public void run() {
						throw new AssertionError(
								"Unknown handler message received: " + msg.what);
					}
				});
			}
		}
	}

	static class DispatcherThread extends HandlerThread {
		DispatcherThread() {
			super(Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME,
					THREAD_PRIORITY_BACKGROUND);
		}
	}

	private class NetworkBroadcastReceiver extends BroadcastReceiver {
		private static final String EXTRA_AIRPLANE_STATE = "state";

		private final ConnectivityManager connectivityManager;

		NetworkBroadcastReceiver(Context context) {
			connectivityManager = (ConnectivityManager) context
					.getSystemService(CONNECTIVITY_SERVICE);
		}

		void register() {
			boolean shouldScanState = service instanceof PicassoExecutorService
					&& //
					Utils.hasPermission(context,
							Manifest.permission.ACCESS_NETWORK_STATE);
			IntentFilter filter = new IntentFilter();
			filter.addAction(ACTION_AIRPLANE_MODE_CHANGED);
			if (shouldScanState) {
				filter.addAction(CONNECTIVITY_ACTION);
			}
			context.registerReceiver(this, filter);
		}

		void unregister() {
			context.unregisterReceiver(this);
		}

		@Override
		public void onReceive(Context context, Intent intent) {
			// On some versions of Android this may be called with a null Intent
			if (null == intent) {
				return;
			}

			String action = intent.getAction();
			Bundle extras = intent.getExtras();

			if (ACTION_AIRPLANE_MODE_CHANGED.equals(action)) {
				dispatchAirplaneModeChange(extras.getBoolean(
						EXTRA_AIRPLANE_STATE, false));
			} else if (CONNECTIVITY_ACTION.equals(action)) {
				dispatchNetworkStateChange(connectivityManager
						.getActiveNetworkInfo());
			}
		}
	}
}
